package web.suzy.oj.manager.oj;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import web.suzy.oj.common.exception.StatusAccessDeniedException;
import web.suzy.oj.common.exception.StatusFailException;
import web.suzy.oj.common.exception.StatusForbiddenException;
import web.suzy.oj.config.NacosSwitchConfig;
import web.suzy.oj.config.WebConfig;
import web.suzy.oj.dao.user.SessionEntityService;
import web.suzy.oj.dao.user.SysUserRoleEntityService;
import web.suzy.oj.dao.user.UserInfoEntityService;
import web.suzy.oj.dao.user.UserRecordEntityService;
import web.suzy.oj.manager.email.EmailManager;
import web.suzy.oj.manager.msg.NoticeManager;
import web.suzy.oj.pojo.dto.ApplyResetPasswordDTO;
import web.suzy.oj.pojo.dto.LoginDTO;
import web.suzy.oj.pojo.dto.RegisterDTO;
import web.suzy.oj.pojo.dto.ResetPasswordDTO;
import web.suzy.oj.pojo.entity.user.*;
import web.suzy.oj.pojo.vo.RegisterCodeVO;
import web.suzy.oj.pojo.vo.SysUserRoleVO;
import web.suzy.oj.pojo.vo.UserInfoVO;
import web.suzy.oj.shiro.AccountProfile;
import web.suzy.oj.utils.Constants;
import web.suzy.oj.utils.IpUtils;
import web.suzy.oj.utils.JwtUtils;
import web.suzy.oj.utils.RedisUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.stream.Collectors;

/**
 * YangSuzy 软件工作室
 * 类名: PassportManager
 * 描述: 用户通行证管理器
 * 功能: 处理用户登录登出、注册、修改密码方法
 * =================未完成===================
 * 1.IP异地校验 2.user_record
 *
 * @author YangSuzy
 * Date: 2022/11/9 13:46
 */
@Component
public class PassportManager {

    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private NacosSwitchConfig nacosSwitchConfig;

    @Resource
    private UserInfoEntityService userInfoEntityService;

    @Resource
    private SysUserRoleEntityService sysUserRoleEntityService;

    @Resource
    private UserRecordEntityService userRecordEntityService;

    @Resource
    private SessionEntityService sessionEntityService;

    @Resource
    private EmailManager emailManager;

    @Resource
    private NoticeManager noticeManager;

    /**
     * 方法名: login
     * 描述: 用户登录方法
     * ========未完成=========
     * 1.IP异地登录校验
     *
     * @param loginDto 登录数据实体类
     * @param response 响应
     * @param request  请求
     * @return web.suzy.oj.pojo.vo.UserInfoVO
     * @date 2022/11/9 13:55
     * @auther YangSuzy
     **/
    public UserInfoVO login(LoginDTO loginDto, HttpServletResponse response, HttpServletRequest request) throws StatusFailException {
        // 去掉账号密码首尾的空格
        loginDto.setPassword(loginDto.getPassword().trim());
        loginDto.setUsername(loginDto.getUsername().trim());

        if (StringUtils.isEmpty(loginDto.getUsername()) || StringUtils.isEmpty(loginDto.getPassword())) {
            throw new StatusFailException("用户名或密码不能为空！");
        }

        if (loginDto.getPassword().length() < 6 || loginDto.getPassword().length() > 20) {
            throw new StatusFailException("密码长度应该为6~20位！");
        }
        if (loginDto.getUsername().length() > 20) {
            throw new StatusFailException("用户名长度不能超过20位!");
        }

        //根据用户IP设置缓存Key，记录尝试登录次数
        String userIpAddr = IpUtils.getUserIpAddr(request);
        String key = Constants.Account.TRY_LOGIN_NUM.getCode() + loginDto.getUsername() + "_" + userIpAddr;
        Integer tryLoginCount = (Integer) redisUtils.get(key);
        //尝试登录次数过多锁定登录功能
        if (tryLoginCount != null && tryLoginCount >= 20) {
            throw new StatusFailException("对不起！登录失败次数过多！您的账号有风险，30分钟内暂时无法登录！");
        }

        //根据登录名获取用户角色信息视图对象
        SysUserRoleVO sysUserRoleVo = sysUserRoleEntityService.getUserRoles(null, loginDto.getUsername());

        if (sysUserRoleVo == null) {
            throw new StatusFailException("用户名或密码错误！请注意大小写！");
        }
        if (!sysUserRoleVo.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
            if (tryLoginCount == null) {
                //第一次登录失败设置计数缓存，30分钟不尝试该限制会自动清空消失
                redisUtils.set(key, 1, 60 * 30);
            } else {
                redisUtils.set(key, tryLoginCount + 1, 60 * 30);
            }
            throw new StatusFailException("用户名或密码错误！请注意大小写！");
        }
        if (sysUserRoleVo.getStatus() != 0) {
            throw new StatusFailException("该账户已被封禁，请联系管理员进行处理！");
        }

        //获取用户登录凭证
        String jwt = jwtUtils.generateToken(sysUserRoleVo.getUid());
        //放到信息头部
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");

        //保存用户登录会话记录
        sessionEntityService.save(new Session()
                .setUid(sysUserRoleVo.getUid())
                .setIp(IpUtils.getUserIpAddr(request))
                .setUserAgent(request.getHeader("User-Agent")));

        //登录成功后清除锁定限制
        if (tryLoginCount != null) {
            redisUtils.del(key);
        }

        UserInfoVO userInfoVo = new UserInfoVO();
        BeanUtil.copyProperties(sysUserRoleVo, userInfoVo, "roles");
        userInfoVo.setRoleList(sysUserRoleVo.getRoles().stream().map(SysRole::getRole).collect(Collectors.toList()));
        return userInfoVo;
    }


    /**
     * 方法名: getRegisterCode
     * 描述: 向注册用户发送注册验证码
     * ============未完成===========
     * 1.微服务
     *
     * @param email 注册用户邮箱
     * @return web.suzy.oj.pojo.vo.RegisterCodeVO
     * @date 2022/11/14 11:41
     * @auther YangSuzy
     **/
    public RegisterCodeVO getRegisterCode(String email) throws StatusAccessDeniedException, StatusFailException, StatusForbiddenException {
        WebConfig webConfig = nacosSwitchConfig.getWebConfig();
        //判断注册功能是否开启
        if (!webConfig.getRegister()) {
            throw new StatusAccessDeniedException("对不起！本站暂未开启注册功能！");
        }
        //判断邮箱是否配置
        if (!emailManager.isOk()) {
            throw new StatusAccessDeniedException("对不起！本站邮箱系统未配置，暂不支持注册！");
        }
        email = email.trim();
        boolean isEmail = Validator.isEmail(email);
        if (!isEmail) {
            throw new StatusFailException("对不起！您的邮箱格式不正确！");
        }
        //根据输入邮箱从缓存中获取邮箱注册锁
        String lockKey = Constants.Email.REGISTER_EMAIL_LOCK + email;
        if (redisUtils.hasKey(lockKey)) {
            throw new StatusFailException("对不起，您的操作频率过快，请在" + redisUtils.getExpire(lockKey) + "秒后再次发送注册邮件！");
        }
        //校验邮箱是否已被注册
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("email", email);
        UserInfo userInfo = userInfoEntityService.getOne(queryWrapper, false);
        if (userInfo != null) {
            throw new StatusFailException("对不起！该邮箱已被注册，请更换新的邮箱！");
        }
        //随机生成6位数字验证码
        String numbers = RandomUtil.randomNumbers(6);
        //设置验证码和有效期10分钟
        redisUtils.set(Constants.Email.REGISTER_KEY_PREFIX.getValue() + email, numbers, 10 * 60);
        //发送邮件
        emailManager.sendCode(email, numbers);
        //设置1分钟的邮箱注册锁
        redisUtils.set(lockKey, 0, 60);
        //返回注册验证码视图对象
        RegisterCodeVO registerCodeVo = new RegisterCodeVO();
        registerCodeVo.setEmail(email);
        registerCodeVo.setExpire(10 * 60);
        return registerCodeVo;
    }

    /**
     * 方法名: register
     * 描述: 用户注册方法
     *
     * @param registerDto 注册用户数据传输对象
     * @return void
     * @date 2022/11/9 21:39
     * @auther YangSuzy
     **/
    @Transactional(rollbackFor = Exception.class) //抛出异常时回滚
    public void register(RegisterDTO registerDto) throws StatusAccessDeniedException, StatusFailException {
        WebConfig webConfig = nacosSwitchConfig.getWebConfig();
        //判断注册功能是否开启
        if (!webConfig.getRegister()) {
            throw new StatusAccessDeniedException("对不起！本站暂未开启注册功能！");
        }
        //从缓存查看验证码是否过期
        String codeKey = Constants.Email.REGISTER_KEY_PREFIX.getValue() + registerDto.getEmail();
        if (!redisUtils.hasKey(codeKey)) {
            throw new StatusFailException("验证码不存在或已过期");
        }
        //校验验证码
        if (!redisUtils.get(codeKey).equals(registerDto.getCode())) {
            throw new StatusFailException("验证码不正确");
        }
        //校验注册数据
        if (StringUtils.isEmpty(registerDto.getPassword())) {
            throw new StatusFailException("密码不能为空");
        }
        if (registerDto.getPassword().length() < 6 || registerDto.getPassword().length() > 20) {
            throw new StatusFailException("密码长度应该为6~20位！");
        }
        if (StringUtils.isEmpty(registerDto.getUsername())) {
            throw new StatusFailException("用户名不能为空");
        }
        if (registerDto.getUsername().length() > 20) {
            throw new StatusFailException("用户名长度不能超过20位!");
        }

        //为新用户设置uuid
        String uuid = IdUtil.simpleUUID();
        registerDto.setUuid(uuid);
        //将密码MD5加密写入数据库
        registerDto.setPassword(SecureUtil.md5(registerDto.getPassword().trim()));
        registerDto.setUsername(registerDto.getUsername().trim());
        registerDto.setEmail(registerDto.getEmail().trim());

        //往user_info表插入数据
        boolean addUser = userInfoEntityService.addUser(registerDto);
        //往user_role表插入数据
        boolean addUserRole = sysUserRoleEntityService.save(new SysUserRole().setRoleId(1002L).setUid(uuid));

        //往user_record表插入数据
        boolean addUserRecord = userRecordEntityService.save(new UserRecord().setUid(uuid));

        if (addUser && addUserRole && addUserRecord) {
            System.out.println("PassportManager:" + registerDto.getUsername() + "注册成功");
            redisUtils.del(registerDto.getEmail());
            //将公告消息转发给新注册用户
            noticeManager.syncNoticeToNewRegisterUser(uuid);
        } else {
            throw new StatusFailException("注册失败，请稍后重新尝试！");
        }
    }

    /**
     * 方法名: applyResetPassword
     * 描述: 通过邮件发送重置密码链接
     *
     * @param applyResetPasswordDto
     * @return void
     * @date 2022/11/14 12:25
     * @auther YangSuzy
     **/
    public void applyResetPassword(ApplyResetPasswordDTO applyResetPasswordDto) throws StatusFailException {
        //获取前端输入的验证码
        String captcha = applyResetPasswordDto.getCaptcha();
        String captchaKey = applyResetPasswordDto.getCaptchaKey();
        String email = applyResetPasswordDto.getEmail();
        //校验输入数据
        if (StringUtils.isEmpty(captcha) || StringUtils.isEmpty(email) || StringUtils.isEmpty(captchaKey)) {
            throw new StatusFailException("邮箱或验证码不能为空");
        }
        //判断邮箱是否配置
        if (!emailManager.isOk()) {
            throw new StatusFailException("对不起！本站邮箱系统未配置，暂不支持重置密码！");
        }
        //获取邮箱重置锁判断是否能够重置
        String lockKey = Constants.Email.RESET_EMAIL_LOCK + email;
        if (redisUtils.hasKey(lockKey)) {
            throw new StatusFailException("对不起，您的操作频率过快，请在" + redisUtils.getExpire(lockKey) + "秒后再次发送重置邮件！");
        }
        //获取redis中的前端验证码并校验
        String redisCode = (String) redisUtils.get(captchaKey);
        if (!redisCode.equals(captcha.trim().toLowerCase())) {
            throw new StatusFailException("验证码不正确");
        }
        //判断输入邮箱是否注册
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
        userInfoQueryWrapper.eq("email", email.trim());
        UserInfo userInfo = userInfoEntityService.getOne(userInfoQueryWrapper, false);
        if (userInfo == null) {
            throw new StatusFailException("对不起，该邮箱无该用户，请重新检查！");
        }
        //随机生成20位数字与字母的组合
        String code = IdUtil.simpleUUID().substring(0, 21);
        //设置重置链接和有效期10分钟
        redisUtils.set(Constants.Email.RESET_PASSWORD_KEY_PREFIX.getValue() + userInfo.getUsername(), code, 10 * 60);
        //发送邮件
        emailManager.sendResetPassword(userInfo.getUsername(), code, email.trim());
        //设置90秒的邮箱重置锁
        redisUtils.set(lockKey, 0, 90);
    }

    /**
     * 方法名: resetPassword
     * 描述: 密码重置方法
     *
     * @param resetPasswordDto 密码重置数据传输对象
     * @return void
     * @date 2022/11/9 22:08
     * @auther YangSuzy
     **/
    public void resetPassword(ResetPasswordDTO resetPasswordDto) throws StatusFailException {
        String username = resetPasswordDto.getUsername();
        String password = resetPasswordDto.getPassword();
        String code = resetPasswordDto.getCode();

        if (StringUtils.isEmpty(password) || StringUtils.isEmpty(username) || StringUtils.isEmpty(code)) {
            throw new StatusFailException("用户名、新密码或验证码不能为空");
        }

        if (password.length() < 6 || password.length() > 20) {
            throw new StatusFailException("新密码长度应该为6~20位！");
        }

        //从缓存中获取重置密码链接
        String codeKey = Constants.Email.RESET_PASSWORD_KEY_PREFIX.getValue() + username;
        if (!redisUtils.hasKey(codeKey)) {
            throw new StatusFailException("重置密码链接不存在或已过期，请重新发送重置邮件");
        }
        //校验验证码
        if (!redisUtils.get(codeKey).equals(code)) {
            throw new StatusFailException("重置密码的验证码不正确，请重新输入");
        }

        UpdateWrapper<UserInfo> userInfoUpdateWrapper = new UpdateWrapper<>();
        //添加条件:用户名相等
        userInfoUpdateWrapper.eq("username", username).set("password", SecureUtil.md5(password));
        boolean isOk = userInfoEntityService.update(userInfoUpdateWrapper);
        if (!isOk) {
            throw new StatusFailException("重置密码失败");
        }
        redisUtils.del(codeKey);
    }


    /**
     * 方法名: logout
     * 描述: 退出登录，将用户登录凭证从redis中删除
     *
     * @return void
     * @date 2022/11/15 12:48
     * @auther YangSuzy
     **/
    public void logout() {
        AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
        jwtUtils.cleanToken(userRolesVo.getUid());
        SecurityUtils.getSubject().logout();
    }

}
